﻿using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using System;

namespace block_explorer
{
    class Player : DriveableEntity
    {
        private const float JumpPower = 5.0f;
        private const float MaxSpeed = 4.5f;
        private const float MaxFlyingSpeed = 45.0f;

        // this might need to be oriented?
        public static Vector3 EyesOffset = new Vector3(0, 0.7f, 0);

        protected Block mGroundBlock;

        private Vector3 mTarget;
        public override Vector3 Target { get { return mTarget; } }
        private SelectedBlockInfo mSelection;

        private VertexBuffer mSelectedBlockBuffer;
        private VertexBuffer[] mSelectedFacebuffer;

        public Player()
        {
            float width = 0.98f;
            float height = 1.98f;
            Bounds = new BoundingBox(new Vector3(-width * 0.5f, -height * 0.5f, -width * 0.5f), new Vector3(width * 0.5f, height * 0.5f, width * 0.5f));

            mSelection = null;

            Solid = true;
            Massless = true;
            DrivePower = 20.0f;

            mTarget = Position;
        }

        public override void Initialize(GraphicsDevice device, ContentManager content)
        {
            BoundingBox b = new BoundingBox(Vector3.Zero, new Vector3(Block.SIZE, Block.SIZE, Block.SIZE));
            mSelectedBlockBuffer = Util.BoundingBoxEx.CreateWireframe(device, b, Color.Yellow);

            mSelectedFacebuffer = new VertexBuffer[3];
            for (int i = 0; i < 3; ++i)
                mSelectedFacebuffer[i] = new VertexBuffer(device, VertexPositionColor.VertexDeclaration, 6, BufferUsage.WriteOnly);

            Color faceColor = new Color(0.0f, 0.6f, 1.0f, 0.2f);
            VertexPositionColor[] verts = new VertexPositionColor[6];
            verts[0] = new VertexPositionColor(new Vector3(0, 0, 0), faceColor);
            verts[1] = new VertexPositionColor(new Vector3(0, 0, 1), faceColor);
            verts[2] = new VertexPositionColor(new Vector3(0, 1, 1), faceColor);
            verts[3] = new VertexPositionColor(new Vector3(0, 1, 1), faceColor);
            verts[4] = new VertexPositionColor(new Vector3(0, 1, 0), faceColor);
            verts[5] = new VertexPositionColor(new Vector3(0, 0, 0), faceColor);
            mSelectedFacebuffer[0].SetData(verts);

            verts[0] = new VertexPositionColor(new Vector3(0, 0, 0), faceColor);
            verts[1] = new VertexPositionColor(new Vector3(0, 0, 1), faceColor);
            verts[2] = new VertexPositionColor(new Vector3(1, 0, 1), faceColor);
            verts[3] = new VertexPositionColor(new Vector3(1, 0, 1), faceColor);
            verts[4] = new VertexPositionColor(new Vector3(1, 0, 0), faceColor);
            verts[5] = new VertexPositionColor(new Vector3(0, 0, 0), faceColor);
            mSelectedFacebuffer[1].SetData(verts);

            verts[0] = new VertexPositionColor(new Vector3(0, 0, 0), faceColor);
            verts[1] = new VertexPositionColor(new Vector3(0, 1, 0), faceColor);
            verts[2] = new VertexPositionColor(new Vector3(1, 1, 0), faceColor);
            verts[3] = new VertexPositionColor(new Vector3(1, 1, 0), faceColor);
            verts[4] = new VertexPositionColor(new Vector3(1, 0, 0), faceColor);
            verts[5] = new VertexPositionColor(new Vector3(0, 0, 0), faceColor);
            mSelectedFacebuffer[2].SetData(verts);

            base.Initialize(device, content);
        }

        public override void Update(GameTime gameTime)
        {
            // clip the max horizontal speed
            Vector3 horizVel = Velocity;

            if (Solid && !Massless)
                horizVel.Y = 0;

            float maxSpeed = Massless ? MaxFlyingSpeed : MaxSpeed;
            float speed = horizVel.Length();
            if (speed >= maxSpeed)
            {
                horizVel.Normalize();
                horizVel *= maxSpeed;
                Velocity.X = horizVel.X;
                Velocity.Z = horizVel.Z;
                if (!Solid || Massless)
                    Velocity.Y = horizVel.Y;
                speed = maxSpeed;
            }

            // apply friction if on the ground
            if (((!Solid || Massless || mGroundBlock != null) || mDriveForce.LengthSquared() <= 0) && speed > 0)
            {
                // add friction based on surface type
                // TODO
                float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;
                speed -= (speed * 4.0f) * dt;
                if (speed < 0.001f)
                    speed = 0;
                horizVel.Normalize();
                horizVel *= speed;
                Velocity.X = horizVel.X;
                Velocity.Z = horizVel.Z;
                if (!Solid || Massless)
                    Velocity.Y = horizVel.Y;
            }

            base.Update(gameTime);

            Vector3 forward = Vector3.Transform(new Vector3(0, 0, -4), Quaternion.Inverse(Orientation));
            mTarget = Position + EyesOffset + forward;

            UpdateSelectedBlock();

            DoGroundTest();
        }

        protected override void ApplyDriveForce(GameTime gameTime)
        {
            float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;

            Vector3 transformedForce;
            if (Massless || !Solid)
                transformedForce = Vector3.Transform(mDriveForce, Quaternion.Inverse(Orientation));
            else
            {
                transformedForce = mDriveForce;
                transformedForce.Y = 0.0f;
                transformedForce = Vector3.Transform(transformedForce, Quaternion.CreateFromYawPitchRoll(MathHelper.ToRadians(-mYaw), 0.0f, 0.0f));
                if (mGroundBlock != null
                    && Math.Abs(Velocity.Y) <= Single.Epsilon
                    && mDriveForce.Y >= 1.0f)
                    Velocity.Y += JumpPower;
            }

            Velocity += transformedForce * DrivePower * dt;
        }

        private void DoGroundTest()
        {
            mGroundBlock = null;
            Ray downRay = new Ray(Position, new Vector3(0, -1, 0));
            float centerToFloor = -Bounds.Min.Y + (Block.SIZE / 2.0f);
            int maxBlocks = (int)Math.Ceiling(centerToFloor / Block.SIZE) + 1;
            foreach (Vector3 cell in Scene.Map.GetCellsOnRay(downRay, maxBlocks))
            {
                Block block = Scene.Map.GetBlockAt(cell);
                if (block != null && block.IsSolid())
                {
                    float d = Math.Abs(Position.Y - Block.GetCenter(cell).Y);
                    float maxDist = (Block.SIZE / 2.0f) + centerToFloor;
                    if (d <= maxDist + 0.001f)
                    {
                        mGroundBlock = block;
                        break;
                    }
                }
            }
        }

        public override void Draw2D(GraphicsDevice device, Camera camera, Shaders.Render2DEffect effect)
        {
            if (mSelection != null)
            {
                // no backface culling
                RasterizerState oldRasterizerState = device.RasterizerState;
                RasterizerState rasterizerState = new RasterizerState();
                rasterizerState.CullMode = CullMode.None;
                device.RasterizerState = rasterizerState;

                // blend mode
                device.BlendState = BlendState.Additive;

                // render the selected block wireframe
                device.SetVertexBuffer(mSelectedBlockBuffer);
                effect.World = Matrix.CreateTranslation(mSelection.Cell);

                foreach (EffectPass pass in effect.Passes)
                {
                    pass.Apply();
                    device.DrawPrimitives(PrimitiveType.LineList, 0, 12);
                }

                // render the face quad
                VertexBuffer faceBuffer;
                Vector3 offset;
                if (mSelection.FaceNormal.X != 0)
                {
                    faceBuffer = mSelectedFacebuffer[0];
                    offset = new Vector3(mSelection.FaceNormal.X > 0 ? 1.0f : 0.0f, 0.0f, 0.0f);
                }
                else if (mSelection.FaceNormal.Y != 0)
                {
                    faceBuffer = mSelectedFacebuffer[1];
                    offset = new Vector3(0.0f, mSelection.FaceNormal.Y > 0 ? 1.0f : 0.0f, 0.0f);
                }
                else
                {
                    faceBuffer = mSelectedFacebuffer[2];
                    offset = new Vector3(0.0f, 0.0f, mSelection.FaceNormal.Z > 0 ? 1.0f : 0.0f);
                }

                device.SetVertexBuffer(faceBuffer);
                effect.World = Matrix.CreateTranslation(mSelection.Cell + offset);

                foreach (EffectPass pass in effect.Passes)
                {
                    pass.Apply();
                    device.DrawPrimitives(PrimitiveType.TriangleList, 0, 2);
                }

                // undo setup
                device.BlendState = BlendState.Opaque;
                device.RasterizerState = oldRasterizerState;
            }

            base.Draw2D(device, camera, effect);
        }

        private Util.LineSegment GetPickingLine()
        {
            return new Util.LineSegment(Position + EyesOffset, mTarget);
        }

        private void UpdateSelectedBlock()
        {
            // maybe this should be moved away from the player object and into
            // a camera object or something completely different
            mSelection = null;
            Util.LineSegment pickingLine = GetPickingLine();
            Vector3 cell, t;
            if (Scene.Map.IntersectLineSegment(pickingLine.Start, pickingLine.End, out cell, out t))
            {
                Ray r = pickingLine.Ray;
                Vector3 faceNormal = Scene.Map.GetFaceNormal(cell, r);
                mSelection = new SelectedBlockInfo(cell, faceNormal);
            }
        }

        public override void OnInputCommand(InputCommand command)
        {
            base.OnInputCommand(command);

            switch (command.Type)
            {
                case InputCommand.Command.BlockPick:
                    if (mSelection != null)
                        Scene.Map.ModifyBlockAt(mSelection.Cell, Block.ContentType.AIR);
                    break;
                case InputCommand.Command.BlockPlace:
                    if (mSelection != null)
                    {
                        Vector3 cell = mSelection.Cell + mSelection.FaceNormal;
                        Scene.Map.ModifyBlockAt(cell, Block.ContentType.DIRT);
                    }
                    break;
            }
        }
    }

    class SelectedBlockInfo
    {
        private Vector3 mCell;
        private Vector3 mNormal;

        public Vector3 Cell { get { return mCell; } }
        public Vector3 FaceNormal { get { return mNormal; } }

        public SelectedBlockInfo(Vector3 cell, Vector3 faceNormal)
        {
            mCell = cell;
            mNormal = faceNormal;
        }
    }
}
